feat: instances list actions

This commit is contained in:
Sérgio Ramos 2017-10-11 17:59:59 +01:00 committed by Sérgio Ramos
parent 7536cdfc85
commit 6f10428b0f
26 changed files with 561 additions and 116 deletions

View File

@ -23,8 +23,8 @@
"bootstrap": "lerna bootstrap", "bootstrap": "lerna bootstrap",
"dev": "redrun -p dev:*", "dev": "redrun -p dev:*",
"dev:ui-toolkit": "lerna run watch --scope joyent-ui-toolkit", "dev:ui-toolkit": "lerna run watch --scope joyent-ui-toolkit",
"dev:cp-frontend": "lerna run start --scope joyent-cp-frontend", "dev:my-joy-beta": "lerna run dev --scope my-joy-beta",
"dev:gql-mock-server": "lerna run dev --scope joyent-cp-gql-mock-server", "dev:cloudapi-gql": "lerna run dev --scope cloudapi-gql",
"commitmsg": "commitlint -e", "commitmsg": "commitlint -e",
"precommit": "cross-env CI=1 redrun -s lint-staged format-staged", "precommit": "cross-env CI=1 redrun -s lint-staged format-staged",
"postinstall": "lerna run prepublish", "postinstall": "lerna run prepublish",

View File

@ -35,8 +35,9 @@ module.exports.start = uuid => request('startMachine', uuid);
module.exports.startFromSnapshot = ctx => module.exports.startFromSnapshot = ctx =>
request('startMachineFromSnapshot', ctx); request('startMachineFromSnapshot', ctx);
module.exports.reboot = ctx => request('rebootMachine', ctx); module.exports.reboot = ctx => request('rebootMachine', ctx);
module.exports.resize = ctx => request('', ctx); (module.exports.resize = ({ id, package }) =>
module.exports.rename = ctx => request('', ctx); request.fetch(`/:login/machines/${id}?action=resize?package=${package}`)),
(module.exports.rename = ctx => request('', ctx));
module.exports.destroy = ctx => request('deleteMachine', ctx); module.exports.destroy = ctx => request('deleteMachine', ctx);
module.exports.audit = ({ id }) => request('machineAudit', id); module.exports.audit = ({ id }) => request('machineAudit', id);

View File

@ -4,25 +4,35 @@ const api = require('../api');
const resolvers = { const resolvers = {
Query: { Query: {
account: () => api.account.get(), account: () => api.account.get(),
keys: (root, { login, name }) => keys: (root, { login, name }) =>
name name
? api.keys.get({ login, name }).then(key => [key]) ? api.keys.get({ login, name }).then(key => [key])
: api.keys.list({ login, name }), : api.keys.list({ login, name }),
key: (root, { login, name }) => api.keys.get({ login, name }), key: (root, { login, name }) => api.keys.get({ login, name }),
users: (root, { id }) => users: (root, { id }) =>
id ? api.users.get({ id }).then(user => [user]) : api.users.list(), id ? api.users.get({ id }).then(user => [user]) : api.users.list(),
user: (root, { id }) => api.users.get({ id }), user: (root, { id }) => api.users.get({ id }),
roles: (root, { id, name }) => roles: (root, { id, name }) =>
id || name id || name
? api.roles.get({ id, name }).then(role => [role]) ? api.roles.get({ id, name }).then(role => [role])
: api.roles.list(), : api.roles.list(),
role: (root, { id, name }) => api.roles.get({ id, name }), role: (root, { id, name }) => api.roles.get({ id, name }),
policies: (root, { id }) => policies: (root, { id }) =>
id id
? api.policies.get({ id }).then(policy => [policy]) ? api.policies.get({ id }).then(policy => [policy])
: api.policies.list(), : api.policies.list(),
policy: (root, { id }) => api.policies.get({ id }), policy: (root, { id }) => api.policies.get({ id }),
config: () => api.config().then(toKeyValue), config: () => api.config().then(toKeyValue),
datacenters: () => datacenters: () =>
api.datacenters().then(dcs => api.datacenters().then(dcs =>
Object.keys(dcs).map(name => ({ Object.keys(dcs).map(name => ({
@ -30,17 +40,23 @@ const resolvers = {
url: dcs[name] url: dcs[name]
})) }))
), ),
services: () => api.services().then(toKeyValue), services: () => api.services().then(toKeyValue),
images: (root, { id, ...rest }) => images: (root, { id, ...rest }) =>
id id
? api.images.get({ id }).then(image => [image]) ? api.images.get({ id }).then(image => [image])
: api.images.list(rest), : api.images.list(rest),
image: (root, { id }) => api.images.get({ id }), image: (root, { id }) => api.images.get({ id }),
packages: (root, { id, ...rest }) => packages: (root, { id, ...rest }) =>
id id
? api.packages.get({ id }).then(pkg => [pkg]) ? api.packages.get({ id }).then(pkg => [pkg])
: api.packages.list(rest), : api.packages.list(rest),
package: (root, { id, name }) => api.packages.get({ id, name }), package: (root, { id, name }) => api.packages.get({ id, name }),
machines: (root, { id, brand, state, tags, ...rest }) => machines: (root, { id, brand, state, tags, ...rest }) =>
id id
? api.machines.get({ id }).then(machine => [machine]) ? api.machines.get({ id }).then(machine => [machine])
@ -51,36 +67,45 @@ const resolvers = {
tags: fromKeyValue(tags) tags: fromKeyValue(tags)
}) })
), ),
machine: (root, { id }) => api.machines.get({ id }), machine: (root, { id }) => api.machines.get({ id }),
snapshots: (root, { name, machine }) => snapshots: (root, { name, machine }) =>
name name
? api.machines.snapshots ? api.machines.snapshots
.get({ id: machine, name }) .get({ id: machine, name })
.then(snapshot => [snapshot]) .then(snapshot => [snapshot])
: api.machines.snapshots.list({ id: machine }), : api.machines.snapshots.list({ id: machine }),
snapshot: (root, { name, machine }) => snapshot: (root, { name, machine }) =>
api.machines.snapshots.get({ name, id: machine }), api.machines.snapshots.get({ name, id: machine }),
metadata: (root, { machine, name, ...rest }) => metadata: (root, { machine, name, ...rest }) =>
name name
? api.machines.metadata ? api.machines.metadata
.get(Object.assign(rest, { id: machine, key: name })) .get(Object.assign(rest, { id: machine, key: name }))
.then(value => toKeyValue({ [name]: value })) .then(value => toKeyValue({ [name]: value }))
: api.machines.metadata.list({ id: machine }).then(toKeyValue), : api.machines.metadata.list({ id: machine }).then(toKeyValue),
metadataValue: (root, { name, machine }) => metadataValue: (root, { name, machine }) =>
api.machines.metadata api.machines.metadata
.get({ key: name, id: machine }) .get({ key: name, id: machine })
.then(value => toKeyValue({ [name]: value }).shift()), .then(value => toKeyValue({ [name]: value }).shift()),
tags: (root, { machine, name }) => tags: (root, { machine, name }) =>
name name
? api.machines.tags ? api.machines.tags
.get({ id: machine, tag: name }) .get({ id: machine, tag: name })
.then(value => toKeyValue({ [name]: value })) .then(value => toKeyValue({ [name]: value }))
: api.machines.tags.list({ id: machine }).then(toKeyValue), : api.machines.tags.list({ id: machine }).then(toKeyValue),
tag: (root, { machine, name }) => tag: (root, { machine, name }) =>
api.machines.tags api.machines.tags
.get({ id: machine, tag: name }) .get({ id: machine, tag: name })
.then(value => toKeyValue({ [name]: value }).shift()), .then(value => toKeyValue({ [name]: value }).shift()),
actions: (root, { machine }) => api.machines.audit({ id: machine }), actions: (root, { machine }) => api.machines.audit({ id: machine }),
// eslint-disable-next-line camelcase // eslint-disable-next-line camelcase
firewall_rules: (root, { machine, id }) => firewall_rules: (root, { machine, id }) =>
id id
@ -88,15 +113,22 @@ const resolvers = {
: machine : machine
? api.firewall.listByMachine({ id: machine }) ? api.firewall.listByMachine({ id: machine })
: api.firewall.list(), : api.firewall.list(),
// eslint-disable-next-line camelcase // eslint-disable-next-line camelcase
firewall_rule: (root, { id }) => api.firewall.get({ id }), firewall_rule: (root, { id }) => api.firewall.get({ id }),
vlans: (root, { id }) => (id ? api.vlans.get({ id }) : api.vlans.list()), vlans: (root, { id }) => (id ? api.vlans.get({ id }) : api.vlans.list()),
vlan: (root, { id }) => api.vlans.get({ id }), vlan: (root, { id }) => api.vlans.get({ id }),
networks: (root, { id, vlan }) => networks: (root, { id, vlan }) =>
id ? api.networks.get({ id, vlan }) : api.networks.list({ vlan }), id ? api.networks.get({ id, vlan }) : api.networks.list({ vlan }),
network: (root, { id, vlan }) => api.networks.get({ id, vlan }), network: (root, { id, vlan }) => api.networks.get({ id, vlan }),
nics: (root, { machine, mac }) => nics: (root, { machine, mac }) =>
mac ? api.nics.get({ machine, mac }) : api.nics.list({ machine }), mac ? api.nics.get({ machine, mac }) : api.nics.list({ machine }),
nic: (root, { machine, mac }) => api.nics.get({ machine, mac }) nic: (root, { machine, mac }) => api.nics.get({ machine, mac })
}, },
User: { User: {
@ -104,36 +136,50 @@ const resolvers = {
}, },
Machine: { Machine: {
brand: ({ brand }) => (brand ? brand.toUpperCase() : brand), brand: ({ brand }) => (brand ? brand.toUpperCase() : brand),
state: ({ state }) => (state ? state.toUpperCase() : state), state: ({ state }) => (state ? state.toUpperCase() : state),
image: ({ image }) => resolvers.Query.image(null, { id: image }), image: ({ image }) => resolvers.Query.image(null, { id: image }),
// eslint-disable-next-line camelcase // eslint-disable-next-line camelcase
primary_ip: ({ primaryIp }) => primaryIp, primary_ip: ({ primaryIp }) => primaryIp,
tags: ({ id }, { name }) => tags: ({ id }, { name }) =>
resolvers.Query.tags(null, { machine: id, name }), resolvers.Query.tags(null, { machine: id, name }),
metadata: ({ id }, { name }) => metadata: ({ id }, { name }) =>
resolvers.Query.metadata(null, { machine: id, name }), resolvers.Query.metadata(null, { machine: id, name }),
networks: ({ networks }) => networks: ({ networks }) =>
Promise.all(networks.map(id => resolvers.Query.network(null, { id }))), Promise.all(networks.map(id => resolvers.Query.network(null, { id }))),
// eslint-disable-next-line camelcase // eslint-disable-next-line camelcase
package: root => resolvers.Query.package(null, { name: root.package }), package: root => resolvers.Query.package(null, { name: root.package }),
snapshots: ({ id }, { name }) => snapshots: ({ id }, { name }) =>
resolvers.Query.snapshots(null, { machine: id, name }), resolvers.Query.snapshots(null, { machine: id, name }),
// eslint-disable-next-line camelcase // eslint-disable-next-line camelcase
firewall_rules: ({ id: machine }, { id }) => firewall_rules: ({ id: machine }, { id }) =>
resolvers.Query.firewall_rules(null, { machine, id }), resolvers.Query.firewall_rules(null, { machine, id }),
actions: ({ id }) => resolvers.Query.actions(null, { machine: id }) actions: ({ id }) => resolvers.Query.actions(null, { machine: id })
}, },
Image: { Image: {
os: ({ os }) => (os ? os.toUpperCase() : os), os: ({ os }) => (os ? os.toUpperCase() : os),
state: ({ state }) => (state ? state.toUpperCase() : state), state: ({ state }) => (state ? state.toUpperCase() : state),
type: ({ type }) => (type ? type.toUpperCase() : type) type: ({ type }) => (type ? type.toUpperCase() : type)
}, },
Action: { Action: {
name: ({ action }) => action, name: ({ action }) => action,
parameters: ({ parameters }) => toKeyValue(parameters) parameters: ({ parameters }) => toKeyValue(parameters)
}, },
Caller: { Caller: {
type: ({ type }) => (type ? type.toUpperCase() : type), type: ({ type }) => (type ? type.toUpperCase() : type),
// eslint-disable-next-line camelcase // eslint-disable-next-line camelcase
key_id: ({ keyId }) => keyId key_id: ({ keyId }) => keyId
}, },
@ -151,8 +197,39 @@ const resolvers = {
compression ? compression.toUpperCase() : compression compression ? compression.toUpperCase() : compression
}, },
Mutation: { Mutation: {
stopMachine: (root, { id }) =>
api.machines.stop(id).then(() => resolvers.Query.machine(null, { id })),
startMachine: (root, { id }) =>
api.machines.start(id).then(() => resolvers.Query.machine(null, { id })),
rebootMachine: (root, { id }) => rebootMachine: (root, { id }) =>
api.machines.reboot(id).then(() => resolvers.Query.machine(null, { id })) api.machines.reboot(id).then(() => resolvers.Query.machine(null, { id })),
resizeMachine: (root, { id, ...args }) =>
api.machines
.resize({ id, package: args.package })
.then(() => resolvers.Query.machine(null, { id })),
enableMachineFirewall: (root, { id }) =>
api.machines.firewall
.enable(id)
.then(() => resolvers.Query.machine(null, { id })),
disableMachineFirewall: (root, { id }) =>
api.machines.firewall
.disable(id)
.then(() => resolvers.Query.machine(null, { id })),
createMachineSnapshot: (root, { id, name }) =>
api.machines.snapshots
.create({ id, name })
.then(() => resolvers.Query.snapshots(null, { machine: id, name })),
startMachineFromSnapshot: (root, { id, name }) =>
api.machines.snapshots
.startFromSnapshot({ id, name })
.then(() => resolvers.Query.machine(null, { id }))
} }
}; };

View File

@ -31,7 +31,7 @@
"react-router-dom": "^4.2.2", "react-router-dom": "^4.2.2",
"react-styled-flexboxgrid": "^2.0.3", "react-styled-flexboxgrid": "^2.0.3",
"redux": "^3.7.2", "redux": "^3.7.2",
"redux-form": "^7.1.0", "redux-form": "^7.1.1",
"remcalc": "^1.0.9", "remcalc": "^1.0.9",
"styled-components": "^2.2.1", "styled-components": "^2.2.1",
"styled-is": "^1.1.0" "styled-is": "^1.1.0"
@ -51,7 +51,7 @@
"jest-snapshot": "^21.2.1", "jest-snapshot": "^21.2.1",
"jest-styled-components": "^4.7.0", "jest-styled-components": "^4.7.0",
"jest-transform-graphql": "^2.1.0", "jest-transform-graphql": "^2.1.0",
"joyent-react-scripts": "^2.1.1", "joyent-react-scripts": "^2.2.1",
"react-test-renderer": "^16.0.0", "react-test-renderer": "^16.0.0",
"redrun": "^5.9.18", "redrun": "^5.9.18",
"stylelint": "^8.2.0", "stylelint": "^8.2.0",

View File

@ -37,7 +37,7 @@
"react-router-dom": "^4.2.2", "react-router-dom": "^4.2.2",
"redux": "^3.7.2", "redux": "^3.7.2",
"redux-actions": "^2.2.1", "redux-actions": "^2.2.1",
"redux-form": "^7.1.0", "redux-form": "^7.1.1",
"remcalc": "^1.0.9", "remcalc": "^1.0.9",
"styled-components": "^2.2.1", "styled-components": "^2.2.1",
"title-case": "^2.1.1" "title-case": "^2.1.1"
@ -57,7 +57,7 @@
"jest-snapshot": "^21.2.1", "jest-snapshot": "^21.2.1",
"jest-styled-components": "^4.7.0", "jest-styled-components": "^4.7.0",
"jest-transform-graphql": "^2.1.0", "jest-transform-graphql": "^2.1.0",
"joyent-react-scripts": "^2.1.1", "joyent-react-scripts": "^2.2.1",
"react-test-renderer": "^16.0.0", "react-test-renderer": "^16.0.0",
"redrun": "^5.9.18", "redrun": "^5.9.18",
"serve": "^6.2.0", "serve": "^6.2.0",

View File

@ -0,0 +1,41 @@
import React from 'react';
import {
FormGroup,
FormLabel,
Input,
Button,
Message,
MessageTitle,
MessageDescription
} from 'joyent-ui-toolkit';
export default ({
submitting = false,
error,
handleSubmit = () => {},
onCancel = () => {}
}) => {
const _error = error &&
!submitting && (
<Message error>
<MessageTitle>Ooops!</MessageTitle>
<MessageDescription>{error}</MessageDescription>
</Message>
);
return (
<form onSubmit={handleSubmit}>
{_error}
<FormGroup name="name" reduxForm>
<FormLabel>Name (optional)</FormLabel>
<Input placeholder="Snapshot name" />
</FormGroup>
<Button type="button" disabled={submitting} onClick={onCancel} secondary>
Back
</Button>
<Button type="submit" disabled={submitting} loading={submitting}>
Create
</Button>
</form>
);
};

View File

@ -3,3 +3,5 @@ export { default as List } from './list';
export { default as KeyValue } from './key-value'; export { default as KeyValue } from './key-value';
export { default as Network } from './network'; export { default as Network } from './network';
export { default as FirewallRule } from './firewall-rule'; export { default as FirewallRule } from './firewall-rule';
export { default as Resize } from './resize';
export { default as CreateSnapshot } from './create-snapshot';

View File

@ -1,6 +1,7 @@
import React from 'react'; import React from 'react';
import { Row, Col } from 'react-styled-flexboxgrid'; import { Row, Col } from 'react-styled-flexboxgrid';
import forceArray from 'force-array'; import forceArray from 'force-array';
import get from 'lodash.get';
import { import {
FormGroup, FormGroup,
@ -14,13 +15,34 @@ import {
import Item from './item'; import Item from './item';
export default ({ export default ({
instances, instances = [],
selected = [],
loading, loading,
handleChange = () => null, handleChange = () => null,
onAction = () => null, onAction = () => null,
handleSubmit, handleSubmit,
...rest ...rest
}) => { }) => {
const allowedActions = {
stop: selected.some(({ state }) => state === 'RUNNING'),
start: selected.some(({ state }) => state !== 'RUNNING'),
reboot: true,
resize:
selected.length === 1 && selected.every(({ brand }) => brand === 'KVM'),
enableFw: selected.some(({ firewall_enabled }) => !firewall_enabled),
disableFw: selected.some(({ firewall_enabled }) => firewall_enabled),
createSnap: true,
startSnap:
selected.length === 1 &&
selected.every(({ snapshots = [] }) => snapshots.length)
};
const handleActions = ({ target }) =>
onAction({
name: target.value,
items: selected
});
const _instances = forceArray(instances); const _instances = forceArray(instances);
const items = _instances.map((instance, i, all) => ( const items = _instances.map((instance, i, all) => (
@ -83,21 +105,46 @@ export default ({
<FormLabel>&#8291;</FormLabel> <FormLabel>&#8291;</FormLabel>
<Select <Select
value="actions" value="actions"
disabled={!items.length} disabled={!items.length || !selected.length}
onChange={({ target }) => onAction(target.value)} onChange={handleActions}
fluid fluid
> >
<option value="actions" selected disabled> <option value="actions" selected disabled>
&#8801; &#8801;
</option> </option>
<option value="stop">Stop</option> <option value="stop" disabled={!allowedActions.stop}>
<option value="start">Start</option> Stop
<option value="reboot">Reboot</option> </option>
<option value="resize">Resize</option> <option value="start" disabled={!allowedActions.start}>
<option value="enable-fw">Enable Firewall</option> Start
<option value="disable-fw">Disable Firewall</option> </option>
<option value="create-snap">Create Snapshot</option> <option value="reboot" disabled={!allowedActions.reboot}>
<option value="start-snap">Start from Snapshot</option> Reboot
</option>
<option value="resize" disabled={!allowedActions.resize}>
Resize
</option>
<option value="enableFw" disabled={!allowedActions.enableFw}>
Enable Firewall
</option>
<option
value="disableFw"
disabled={!allowedActions.disableFw}
>
Disable Firewall
</option>
<option
value="createSnap"
disabled={!allowedActions.createSnap}
>
Create Snapshot
</option>
<option
value="startSnap"
disabled={!allowedActions.startSnap}
>
Start from Snapshot
</option>
</Select> </Select>
</FormGroup> </FormGroup>
</Col> </Col>

View File

@ -0,0 +1,8 @@
import React from 'react';
import ReactJson from 'react-json-view';
export default ({ instance, packages, handleSubmit }) => (
<form onSubmit={handleSubmit}>
<ReactJson src={{ instance, packages }} />
</form>
);

View File

@ -0,0 +1,94 @@
import React from 'react';
import { reduxForm, SubmissionError } from 'redux-form';
import { connect } from 'react-redux';
import { compose, graphql } from 'react-apollo';
import find from 'lodash.find';
import get from 'lodash.get';
import {
Title,
ViewContainer,
StatusLoader,
Message,
MessageTitle,
MessageDescription
} from 'joyent-ui-toolkit';
import { CreateSnapshot as InstanceCreateSnapshot } from '@components/instances';
import CreateSnapshotMutation from '@graphql/create-snapshot.gql';
import GetInstance from '@graphql/get-instance.gql';
const CreateSnapshot = ({
match,
instance,
loading,
error,
handleSubmit,
handleCancel
}) => {
const _title = <Title>Create Snapshot</Title>;
const _loading = !(loading && !instance) ? null : <StatusLoader />;
const CreateSnapshotForm = reduxForm({
form: `create-snapshot-${match.params.instance}`
})(InstanceCreateSnapshot);
const _error = error &&
!instance &&
!_loading && (
<Message error>
<MessageTitle>Ooops!</MessageTitle>
<MessageDescription>
An error occurred while loading your instance
</MessageDescription>
</Message>
);
const _form = !loading &&
!_error && (
<CreateSnapshotForm onSubmit={handleSubmit} onCancel={handleCancel} />
);
return (
<ViewContainer center={Boolean(_loading)} main>
{_title}
{_loading}
{_error}
{_form}
</ViewContainer>
);
};
export default compose(
graphql(CreateSnapshotMutation, { name: 'createSnapshot' }),
graphql(GetInstance, {
options: ({ match }) => ({
variables: {
name: get(match, 'params.instance')
}
}),
props: ({ data: { loading, error, variables, ...rest } }) => ({
instance: find(get(rest, 'machines', []), ['name', variables.name]),
loading,
error
})
}),
connect(
null,
(dispatch, { history, location, instance, createSnapshot }) => ({
handleCancel: () => history.push(location.pathname.split(/\/\~/).shift()),
handleSubmit: ({ name }) =>
createSnapshot({
variables: { name, id: instance.id }
})
.catch(error => {
throw new SubmissionError({
_error: error.graphQLErrors
.map(({ message }) => message)
.join('\n')
});
})
.then(() => history.push(`/instances/${instance.name}/snapshots`))
})
)
)(CreateSnapshot);

View File

@ -5,3 +5,5 @@ export { default as Metadata } from './metadata';
export { default as Networks } from './networks'; export { default as Networks } from './networks';
export { default as Firewall } from './firewall'; export { default as Firewall } from './firewall';
export { default as Snapshots } from './snapshots'; export { default as Snapshots } from './snapshots';
export { default as Resize } from './resize';
export { default as CreateSnapshot } from './create-snapshot';

View File

@ -57,8 +57,6 @@ const List = ({
</Message> </Message>
) : null; ) : null;
const handleAction = name => onAction({ name, ids: selected });
return ( return (
<ViewContainer main> <ViewContainer main>
{_title} {_title}
@ -66,7 +64,8 @@ const List = ({
<InstanceListForm <InstanceListForm
instances={_instances} instances={_instances}
loading={loading} loading={loading}
onAction={handleAction} onAction={onAction}
selected={selected}
/> />
</ViewContainer> </ViewContainer>
); );
@ -121,7 +120,8 @@ export default compose(
const selected = Object.keys(form) const selected = Object.keys(form)
.map(name => find(values, ['name', name])) .map(name => find(values, ['name', name]))
.filter(Boolean) .filter(Boolean)
.map(({ id }) => id); .map(({ id }) => find(instances, ['id', id]))
.filter(Boolean);
return { return {
...rest, ...rest,
@ -129,27 +129,39 @@ export default compose(
selected selected
}; };
}, },
(dispatch, { instances, ...ownProps }) => ({ (dispatch, { stop, start, reboot, history, location }) => ({
onAction: ({ name, ids = [] }) => { onAction: ({ name, items = [] }) => {
const types = { const types = {
stop: () => stop: () =>
Promise.all(ids.map(id => ownProps.stop({ variables: { id } }))), Promise.all(items.map(({ id }) => stop({ variables: { id } }))),
start: () => start: () =>
Promise.all(ids.map(id => ownProps.start({ variables: { id } }))), Promise.all(items.map(({ id }) => start({ variables: { id } }))),
reboot: () => reboot: () =>
Promise.all(ids.map(id => ownProps.reboot({ variables: { id } }))), Promise.all(items.map(({ id }) => reboot({ variables: { id } }))),
resize: () => null, resize: () =>
'enable-fw': () => null, Promise.resolve(
'disable-fw': () => null, history.push(`/instances/~resize/${items.shift().name}`)
'create-snap': () => null, ),
'start-snap': () => null enableFw: () =>
Promise.all(items.map(({ id }) => enableFw({ variables: { id } }))),
disableFw: () =>
Promise.all(
items.map(({ id }) => disableFw({ variables: { id } }))
),
createSnap: () =>
Promise.resolve(
history.push(`/instances/~create-snapshot/${items.shift().name}`)
),
startSnap: () =>
Promise.resolve(
history.push(`/instances/${items.shift().name}/snapshots`)
)
}; };
const clearSelected = () => const clearSelected = () =>
dispatch( dispatch(
ids.map(id => { items.map(({ name: field }) => {
const form = 'instance-list'; const form = 'instance-list';
const field = get(find(instances, ['id', id]), 'name');
const value = false; const value = false;
if (!field) { if (!field) {

View File

@ -0,0 +1,77 @@
import React from 'react';
import { reduxForm } from 'redux-form';
import { compose, graphql } from 'react-apollo';
import forceArray from 'force-array';
import find from 'lodash.find';
import get from 'lodash.get';
import {
StatusLoader,
Title,
ViewContainer,
Message,
MessageTitle,
MessageDescription
} from 'joyent-ui-toolkit';
import { Resize as InstanceResize } from '@components/instances';
import ListPackages from '@graphql/list-packages.gql';
import ListInstances from '@graphql/list-instances.gql';
import GetInstance from '@graphql/get-instance.gql';
const Resize = ({ match, loading, error, instance, packages }) => {
const ResizeForm = reduxForm({
form: `resize-instance-${match.params.instance}`
})(InstanceResize);
const _packages = forceArray(packages);
const _title = <Title>Resize</Title>;
const _loading = !(loading && (!_packages.length || !instance)) ? null : (
<StatusLoader />
);
const _error = !(error && !_loading) ? null : (
<Message error>
<MessageTitle>Ooops!</MessageTitle>
<MessageDescription>An error occurred</MessageDescription>
</Message>
);
const _resize =
_loading || _error ? null : (
<ResizeForm packages={packages} instance={instance} />
);
return (
<ViewContainer center={Boolean(_loading)} main>
{_title}
{_loading}
{_error}
{_resize}
</ViewContainer>
);
};
export default compose(
graphql(GetInstance, {
options: ({ match }) => ({
variables: {
name: get(match, 'params.instance')
}
}),
props: ({ data: { loading, error, variables, ...rest } }) => ({
instance: find(get(rest, 'machines', []), ['name', variables.name]),
loading,
error
})
}),
graphql(ListPackages, {
props: ({ data: { packages, loading, error } }) => ({
packages: forceArray(packages),
loading,
error
})
})
)(Resize);

View File

@ -1,3 +1,5 @@
mutation createInstanceSnapshot($id: ID!, $name: String) { mutation createInstanceSnapshot($id: ID!, $name: String) {
createMachineSnapshot(id: $id, name: $name) createMachineSnapshot(id: $id, name: $name) {
name
}
} }

View File

@ -1,4 +1,4 @@
query instance($name: String!) { query instance($name: String) {
machines(name: $name) { machines(name: $name) {
id id
name name
@ -8,5 +8,8 @@ query instance($name: String!) {
created created
updated updated
firewall_enabled firewall_enabled
package {
name
}
} }
} }

View File

@ -0,0 +1,14 @@
query packages($id: ID) {
packages(id: $id) {
id
name
memory
disk
swap
lwps
vcpus
version
group
description
}
}

View File

@ -13,5 +13,8 @@ query instances {
package { package {
name name
} }
snapshots {
name
}
} }
} }

View File

@ -0,0 +1,6 @@
query packages {
packages {
id
name
}
}

View File

@ -14,32 +14,55 @@ import {
Metadata as InstanceMetadata, Metadata as InstanceMetadata,
Networks as InstanceNetworks, Networks as InstanceNetworks,
Firewall as InstanceFirewall, Firewall as InstanceFirewall,
Snapshots as InstanceSnapshots Snapshots as InstanceSnapshots,
Resize as InstanceResize,
CreateSnapshot as InstanceCreateSnapshot
} from '@containers/instances'; } from '@containers/instances';
export default () => ( export default () => (
<BrowserRouter> <BrowserRouter>
<PageContainer> <PageContainer>
{/* Header */}
<Route path="*" component={Header} /> <Route path="*" component={Header} />
{/* Breadcrumb */}
<Switch> <Switch>
<Route path="/instances" exact component={Breadcrumb} /> <Route
<Route path="/instances/:instance" component={Breadcrumb} /> path="/instances/~:action/:instance?"
exact
component={Breadcrumb}
/>
<Route path="/instances/:instance?" component={Breadcrumb} />
</Switch> </Switch>
{/* Menu */}
<Switch> <Switch>
<Route path="/instances" exact component={Menu} /> <Route path="/instances/~:action/:id?" exact component={Menu} />
<Route path="/instances/:instance/:section" component={Menu} /> <Route path="/instances/:instance?/:section?" component={Menu} />
</Switch> </Switch>
{/* Instances List */}
<Switch>
<Route path="/instances" exact component={Instances} /> <Route path="/instances" exact component={Instances} />
</Switch>
{/* Instance Sections */}
<Switch>
<Route path="/instances/~:action" component={() => null} />
<Route
path="/instances/:instance/:section?/~create-snapshot"
component={() => null}
/>
<Route <Route
path="/instances/:instance/summary" path="/instances/:instance/summary"
exact exact
component={InstanceSummary} component={InstanceSummary}
/> />
<Route path="/instances/:instance/tags" exact component={InstanceTags} /> <Route
path="/instances/:instance/tags"
exact
component={InstanceTags}
/>
<Route <Route
path="/instances/:instance/metadata" path="/instances/:instance/metadata"
exact exact
@ -69,6 +92,31 @@ export default () => (
/> />
)} )}
/> />
</Switch>
{/* Actions */}
<Switch>
<Route
path="/instances/~resize/:instance"
exact
component={InstanceResize}
/>
<Route
path="/instances/:instance/:section?/~resize"
exact
component={InstanceResize}
/>
<Route
path="/instances/~create-snapshot/:instance"
exact
component={InstanceCreateSnapshot}
/>
<Route
path="/instances/:instance/:section?/~create-snapshot"
exact
component={InstanceCreateSnapshot}
/>
</Switch>
<Route path="/" exact component={() => <Redirect to="/instances" />} /> <Route path="/" exact component={() => <Redirect to="/instances" />} />
</PageContainer> </PageContainer>

View File

@ -31,7 +31,7 @@
"react-router-dom": "^4.2.2", "react-router-dom": "^4.2.2",
"react-styled-flexboxgrid": "^2.0.3", "react-styled-flexboxgrid": "^2.0.3",
"redux": "^3.7.2", "redux": "^3.7.2",
"redux-form": "^7.1.0", "redux-form": "^7.1.1",
"remcalc": "^1.0.9", "remcalc": "^1.0.9",
"styled-components": "^2.2.1", "styled-components": "^2.2.1",
"styled-is": "^1.1.0", "styled-is": "^1.1.0",

View File

@ -46,17 +46,18 @@
"disable-scroll": "^0.3.0", "disable-scroll": "^0.3.0",
"file-loader": "^1.1.5", "file-loader": "^1.1.5",
"fontfaceobserver": "^2.0.13", "fontfaceobserver": "^2.0.13",
"joy-react-broadcast": "^0.6.9",
"joyent-manifest-editor": "^1.4.0", "joyent-manifest-editor": "^1.4.0",
"lodash.difference": "^4.5.0", "lodash.difference": "^4.5.0",
"lodash.differenceby": "^4.8.0", "lodash.differenceby": "^4.8.0",
"lodash.isequal": "^4.5.0", "lodash.isequal": "^4.5.0",
"lodash.isequalwith": "^4.4.0", "lodash.isequalwith": "^4.4.0",
"lodash.isstring": "^4.0.1", "lodash.isstring": "^4.0.1",
"moment": "^2.19.0",
"normalized-styled-components": "^1.0.17", "normalized-styled-components": "^1.0.17",
"pascal-case": "^2.0.1", "pascal-case": "^2.0.1",
"polished": "^1.8.0", "polished": "^1.8.0",
"prop-types": "^15.6.0", "prop-types": "^15.6.0",
"joy-react-broadcast": "^0.6.9",
"react-bundle": "^1.0.4", "react-bundle": "^1.0.4",
"react-input-range": "^1.2.1", "react-input-range": "^1.2.1",
"react-responsive": "^2.0.0", "react-responsive": "^2.0.0",
@ -100,10 +101,10 @@
"react-redux": "^5.0.6", "react-redux": "^5.0.6",
"react-router-dom": "^4.2.2", "react-router-dom": "^4.2.2",
"react-scripts": "^1.0.14", "react-scripts": "^1.0.14",
"react-styleguidist": "^6.0.28", "react-styleguidist": "^6.0.29",
"react-test-renderer": "^16.0.0", "react-test-renderer": "^16.0.0",
"redux": "^3.7.2", "redux": "^3.7.2",
"redux-form": "^7.1.0", "redux-form": "^7.1.1",
"serve-static": "^1.13.1", "serve-static": "^1.13.1",
"snapguidist": "^2.1.0", "snapguidist": "^2.1.0",
"style-loader": "^0.19.0", "style-loader": "^0.19.0",
@ -121,6 +122,6 @@
"react": "^16.0.0", "react": "^16.0.0",
"react-dom": "^16.0.0", "react-dom": "^16.0.0",
"react-router-dom": "^4.2.2", "react-router-dom": "^4.2.2",
"redux-form": "^7.1.0" "redux-form": "^7.1.1"
} }
} }

View File

@ -27,9 +27,13 @@ const style = css`
`}; `};
`; `;
const StyledAnchor = A.extend`${style};`; const StyledAnchor = A.extend`
${style};
`;
const StyledLink = styled(BaseLink)`${style};`; const StyledLink = styled(BaseLink)`
${style};
`;
/** /**
* @example ./usage.md * @example ./usage.md

View File

@ -11,9 +11,19 @@ export const tooltipShadow = `0 ${remcalc(2)} ${remcalc(6)} ${remcalc(
export const modalShadow = `0 0 ${remcalc(6)} ${remcalc(1)} rgba(0, 0, 0, 0.1)`; export const modalShadow = `0 0 ${remcalc(6)} ${remcalc(1)} rgba(0, 0, 0, 0.1)`;
export const border = { export const border = {
checked: css`${remcalc(1)} solid ${props => props.theme.primary};`, checked: css`
unchecked: css`${remcalc(1)} solid ${props => props.theme.grey};`, ${remcalc(1)} solid ${props => props.theme.primary};
confirmed: css`${remcalc(1)} solid ${props => props.theme.grey};`, `,
error: css`${remcalc(1)} solid ${props => props.theme.red};`, unchecked: css`
secondary: css`${remcalc(1)} solid ${props => props.theme.secondaryActive};` ${remcalc(1)} solid ${props => props.theme.grey};
`,
confirmed: css`
${remcalc(1)} solid ${props => props.theme.grey};
`,
error: css`
${remcalc(1)} solid ${props => props.theme.red};
`,
secondary: css`
${remcalc(1)} solid ${props => props.theme.secondaryActive};
`
}; };

View File

@ -202,7 +202,7 @@ const Button = props => {
const View = Views.reduce((sel, view) => (sel ? sel : view()), null); const View = Views.reduce((sel, view) => (sel ? sel : view()), null);
const children = loading ? ( const children = loading ? (
<StatusLoader secondary={!secondary} small/> <StatusLoader secondary={!secondary} small />
) : ( ) : (
props.children props.children
); );

View File

@ -61,12 +61,9 @@ const GraphNodeInfo = ({ data, pos }) => {
const healthy = ( const healthy = (
<HealthyIcon <HealthyIcon
healthy={ healthy={
instancesHealthy && instancesHealthy && instancesHealthy.total === instancesHealthy.healthy
instancesHealthy.total === instancesHealthy.healthy ? ( ? 'HEALTHY'
'HEALTHY' : 'UNHEALTHY'
) : (
'UNHEALTHY'
)
} }
/> />
); );

View File

@ -392,7 +392,7 @@ apollo-server-core@^1.1.6:
dependencies: dependencies:
apollo-tracing "^0.0.7" apollo-tracing "^0.0.7"
apollo-server-hapi@^1.1.3: apollo-server-hapi@^1.1.6:
version "1.1.6" version "1.1.6"
resolved "https://registry.yarnpkg.com/apollo-server-hapi/-/apollo-server-hapi-1.1.6.tgz#97bdc483afe908e28aa0ae9a3ee7744d581bc3bf" resolved "https://registry.yarnpkg.com/apollo-server-hapi/-/apollo-server-hapi-1.1.6.tgz#97bdc483afe908e28aa0ae9a3ee7744d581bc3bf"
dependencies: dependencies:
@ -1767,7 +1767,7 @@ babel-preset-jest@^21.2.0:
babel-plugin-jest-hoist "^21.2.0" babel-plugin-jest-hoist "^21.2.0"
babel-plugin-syntax-object-rest-spread "^6.13.0" babel-plugin-syntax-object-rest-spread "^6.13.0"
babel-preset-joyent-portal@^3.0.1: babel-preset-joyent-portal@^3.1.0:
version "3.2.0" version "3.2.0"
resolved "https://registry.yarnpkg.com/babel-preset-joyent-portal/-/babel-preset-joyent-portal-3.2.0.tgz#0801746916568886beba5c2911ce1c55ec142320" resolved "https://registry.yarnpkg.com/babel-preset-joyent-portal/-/babel-preset-joyent-portal-3.2.0.tgz#0801746916568886beba5c2911ce1c55ec142320"
dependencies: dependencies:
@ -4363,11 +4363,7 @@ escope@^3.6.0:
esrecurse "^4.1.0" esrecurse "^4.1.0"
estraverse "^4.1.1" estraverse "^4.1.1"
eslint-config-joyent-portal@3.0.0: eslint-config-joyent-portal@3.1.0, eslint-config-joyent-portal@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/eslint-config-joyent-portal/-/eslint-config-joyent-portal-3.0.0.tgz#269e3e0b88abba96adc3a6dc0bbf604a6ae89356"
eslint-config-joyent-portal@^3.0.0:
version "3.1.0" version "3.1.0"
resolved "https://registry.yarnpkg.com/eslint-config-joyent-portal/-/eslint-config-joyent-portal-3.1.0.tgz#7a77270d118627e59461db29d35639cf146d1dfc" resolved "https://registry.yarnpkg.com/eslint-config-joyent-portal/-/eslint-config-joyent-portal-3.1.0.tgz#7a77270d118627e59461db29d35639cf146d1dfc"
@ -5832,7 +5828,7 @@ graphql-tools@^1.1.0:
optionalDependencies: optionalDependencies:
"@types/graphql" "^0.9.0" "@types/graphql" "^0.9.0"
graphql-tools@^2.2.1: graphql-tools@^2.3.0:
version "2.4.0" version "2.4.0"
resolved "https://registry.yarnpkg.com/graphql-tools/-/graphql-tools-2.4.0.tgz#183d7e509e1ebd07d51db05fdeb181e7126f7ecb" resolved "https://registry.yarnpkg.com/graphql-tools/-/graphql-tools-2.4.0.tgz#183d7e509e1ebd07d51db05fdeb181e7126f7ecb"
dependencies: dependencies:
@ -7492,7 +7488,7 @@ joy-react-broadcast@^0.6.9:
prop-types "^15.5.6" prop-types "^15.5.6"
warning "^3.0.0" warning "^3.0.0"
joyent-manifest-editor@^1.3.0: joyent-manifest-editor@^1.4.0:
version "1.4.0" version "1.4.0"
resolved "https://registry.yarnpkg.com/joyent-manifest-editor/-/joyent-manifest-editor-1.4.0.tgz#0c02efe6c02b0386a5b209ae4ddcc3492b9c22ac" resolved "https://registry.yarnpkg.com/joyent-manifest-editor/-/joyent-manifest-editor-1.4.0.tgz#0c02efe6c02b0386a5b209ae4ddcc3492b9c22ac"
dependencies: dependencies:
@ -7500,7 +7496,7 @@ joyent-manifest-editor@^1.3.0:
prop-types "^15.6.0" prop-types "^15.6.0"
react-codemirror "^1.0.0" react-codemirror "^1.0.0"
joyent-react-scripts@^2.0.2: joyent-react-scripts@^2.2.1:
version "2.3.0" version "2.3.0"
resolved "https://registry.yarnpkg.com/joyent-react-scripts/-/joyent-react-scripts-2.3.0.tgz#9e48f93d67284b8149dc73b35ccdfc11d27131d9" resolved "https://registry.yarnpkg.com/joyent-react-scripts/-/joyent-react-scripts-2.3.0.tgz#9e48f93d67284b8149dc73b35ccdfc11d27131d9"
dependencies: dependencies:
@ -8913,7 +8909,7 @@ normalize-url@^1.4.0:
query-string "^4.1.0" query-string "^4.1.0"
sort-keys "^1.0.0" sort-keys "^1.0.0"
normalized-styled-components@^1.0.14: normalized-styled-components@^1.0.17:
version "1.0.17" version "1.0.17"
resolved "https://registry.yarnpkg.com/normalized-styled-components/-/normalized-styled-components-1.0.17.tgz#fd3a82e00b87d0c89d973f795cdaa7b5025ebb8a" resolved "https://registry.yarnpkg.com/normalized-styled-components/-/normalized-styled-components-1.0.17.tgz#fd3a82e00b87d0c89d973f795cdaa7b5025ebb8a"
dependencies: dependencies:
@ -10462,7 +10458,7 @@ react-styled-flexboxgrid@^2.0.3:
dependencies: dependencies:
lodash.isinteger "^4.0.4" lodash.isinteger "^4.0.4"
react-styleguidist@^6.0.28: react-styleguidist@^6.0.29:
version "6.0.30" version "6.0.30"
resolved "https://registry.yarnpkg.com/react-styleguidist/-/react-styleguidist-6.0.30.tgz#988a09282f8af43749e44602349ec524dc1f07a0" resolved "https://registry.yarnpkg.com/react-styleguidist/-/react-styleguidist-6.0.30.tgz#988a09282f8af43749e44602349ec524dc1f07a0"
dependencies: dependencies:
@ -10780,7 +10776,7 @@ reduce-css-calc@^1.2.6:
math-expression-evaluator "^1.2.14" math-expression-evaluator "^1.2.14"
reduce-function-call "^1.0.1" reduce-function-call "^1.0.1"
reduce-css-calc@^2.0.5: reduce-css-calc@^2.1.0:
version "2.1.1" version "2.1.1"
resolved "https://registry.yarnpkg.com/reduce-css-calc/-/reduce-css-calc-2.1.1.tgz#f4ecd7a00ec3e5683773f208067ad7da117b9db0" resolved "https://registry.yarnpkg.com/reduce-css-calc/-/reduce-css-calc-2.1.1.tgz#f4ecd7a00ec3e5683773f208067ad7da117b9db0"
dependencies: dependencies:
@ -10806,7 +10802,7 @@ redux-actions@^2.2.1:
lodash-es "^4.17.4" lodash-es "^4.17.4"
reduce-reducers "^0.1.0" reduce-reducers "^0.1.0"
redux-form@^7.1.0: redux-form@^7.1.1:
version "7.1.1" version "7.1.1"
resolved "https://registry.yarnpkg.com/redux-form/-/redux-form-7.1.1.tgz#4d9ab1d9c03beb3a8b5f8e5d0f398cff4209081f" resolved "https://registry.yarnpkg.com/redux-form/-/redux-form-7.1.1.tgz#4d9ab1d9c03beb3a8b5f8e5d0f398cff4209081f"
dependencies: dependencies:
@ -11375,7 +11371,7 @@ ripemd160@^2.0.0, ripemd160@^2.0.1:
hash-base "^2.0.0" hash-base "^2.0.0"
inherits "^2.0.1" inherits "^2.0.1"
rnd-id@^1.0.8: rnd-id@^1.1.1:
version "1.1.1" version "1.1.1"
resolved "https://registry.yarnpkg.com/rnd-id/-/rnd-id-1.1.1.tgz#aaa11c650cf4105eeb1025eecf185db89071afb6" resolved "https://registry.yarnpkg.com/rnd-id/-/rnd-id-1.1.1.tgz#aaa11c650cf4105eeb1025eecf185db89071afb6"
dependencies: dependencies:
@ -12225,11 +12221,11 @@ styled-components@^2.2.1:
stylis "^3.2.1" stylis "^3.2.1"
supports-color "^3.2.3" supports-color "^3.2.3"
styled-is@^1.0.15: styled-is@^1.1.0:
version "1.1.0" version "1.1.0"
resolved "https://registry.yarnpkg.com/styled-is/-/styled-is-1.1.0.tgz#0cf8d32098fe6559eb0ec889790cc6c84f1f497f" resolved "https://registry.yarnpkg.com/styled-is/-/styled-is-1.1.0.tgz#0cf8d32098fe6559eb0ec889790cc6c84f1f497f"
stylelint-config-joyent-portal@^2.0.0: stylelint-config-joyent-portal@^2.0.1:
version "2.0.1" version "2.0.1"
resolved "https://registry.yarnpkg.com/stylelint-config-joyent-portal/-/stylelint-config-joyent-portal-2.0.1.tgz#9d9242807749db394b9b9c3da7bc48b9b818a16e" resolved "https://registry.yarnpkg.com/stylelint-config-joyent-portal/-/stylelint-config-joyent-portal-2.0.1.tgz#9d9242807749db394b9b9c3da7bc48b9b818a16e"
dependencies: dependencies:
@ -13019,7 +13015,7 @@ unist-util-visit@^1.0.0, unist-util-visit@^1.1.0, unist-util-visit@^1.1.3:
version "1.1.3" version "1.1.3"
resolved "https://registry.yarnpkg.com/unist-util-visit/-/unist-util-visit-1.1.3.tgz#ec268e731b9d277a79a5b5aa0643990e405d600b" resolved "https://registry.yarnpkg.com/unist-util-visit/-/unist-util-visit-1.1.3.tgz#ec268e731b9d277a79a5b5aa0643990e405d600b"
unitcalc@^1.1.0: unitcalc@^1.1.1:
version "1.1.1" version "1.1.1"
resolved "https://registry.yarnpkg.com/unitcalc/-/unitcalc-1.1.1.tgz#a57e1c9dd61f251d2fad0c1d19f8577255cf080a" resolved "https://registry.yarnpkg.com/unitcalc/-/unitcalc-1.1.1.tgz#a57e1c9dd61f251d2fad0c1d19f8577255cf080a"
dependencies: dependencies: