feat(my-joy-beta): add loading and error to instance list actions

This commit is contained in:
Sérgio Ramos 2017-10-13 20:51:18 +01:00 committed by Sérgio Ramos
parent 966d326f3f
commit 55811372fb
4 changed files with 161 additions and 68 deletions

View File

@ -10,7 +10,8 @@ import {
CardView, CardView,
Checkbox, Checkbox,
FormGroup, FormGroup,
QueryBreakpoints QueryBreakpoints,
StatusLoader
} from 'joyent-ui-toolkit'; } from 'joyent-ui-toolkit';
const { SmallOnly, Small } = QueryBreakpoints; const { SmallOnly, Small } = QueryBreakpoints;
@ -24,7 +25,8 @@ const stateColor = {
FAILED: 'red' FAILED: 'red'
}; };
export default ({ name, state, primary_ip, last, first }) => ( // eslint-disable-next-line camelcase
export default ({ name, state, primary_ip, loading, last, first }) => (
<Card collapsed flat={!last} topMargin={first} bottomless={!last} gapless> <Card collapsed flat={!last} topMargin={first} bottomless={!last} gapless>
<CardView> <CardView>
<CardMeta> <CardMeta>
@ -34,23 +36,34 @@ export default ({ name, state, primary_ip, last, first }) => (
</FormGroup> </FormGroup>
</CardAction> </CardAction>
<CardTitle to={`/instances/${name}`}>{name}</CardTitle> <CardTitle to={`/instances/${name}`}>{name}</CardTitle>
<Small> {loading && (
<CardLabel>{primary_ip}</CardLabel> <CardLabel>
</Small> <StatusLoader small />
<Small>
<CardLabel
color={stateColor[state]}
title={`The instance is ${state}`}
>
{titleCase(state)}
</CardLabel> </CardLabel>
</Small> )}
<SmallOnly> {!loading && (
<CardLabel <Small>
color={stateColor[state]} <CardLabel>{primary_ip}</CardLabel>
title={`The instance is ${state}`} </Small>
/> )}
</SmallOnly> {!loading && (
<Small>
<CardLabel
color={stateColor[state]}
title={`The instance is ${state}`}
>
{titleCase(state)}
</CardLabel>
</Small>
)}
{!loading && (
<SmallOnly>
<CardLabel
color={stateColor[state]}
title={`The instance is ${state}`}
/>
</SmallOnly>
)}
</CardMeta> </CardMeta>
</CardView> </CardView>
</Card> </Card>

View File

@ -1,7 +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 find from 'lodash.find';
import { import {
FormGroup, FormGroup,
@ -9,18 +9,28 @@ import {
FormLabel, FormLabel,
ViewContainer, ViewContainer,
StatusLoader, StatusLoader,
Select Select,
Message,
MessageTitle,
MessageDescription,
Button,
QueryBreakpoints
} from 'joyent-ui-toolkit'; } from 'joyent-ui-toolkit';
import Item from './item'; import Item from './item';
const { SmallOnly, Medium } = QueryBreakpoints;
export default ({ export default ({
instances = [], instances = [],
selected = [], selected = [],
loading, loading,
error,
handleChange = () => null, handleChange = () => null,
onAction = () => null, onAction = () => null,
handleSubmit, handleSubmit,
submitting = false,
pristine = true,
...rest ...rest
}) => { }) => {
const allowedActions = { const allowedActions = {
@ -29,30 +39,44 @@ export default ({
reboot: true, reboot: true,
resize: resize:
selected.length === 1 && selected.every(({ brand }) => brand === 'KVM'), selected.length === 1 && selected.every(({ brand }) => brand === 'KVM'),
// eslint-disable-next-line camelcase
enableFw: selected.some(({ firewall_enabled }) => !firewall_enabled), enableFw: selected.some(({ firewall_enabled }) => !firewall_enabled),
// eslint-disable-next-line camelcase
disableFw: selected.some(({ firewall_enabled }) => firewall_enabled), disableFw: selected.some(({ firewall_enabled }) => firewall_enabled),
createSnap: true, createSnap: selected.length === 1,
startSnap: startSnap:
selected.length === 1 && selected.length === 1 &&
selected.every(({ snapshots = [] }) => snapshots.length) selected.every(({ snapshots = [] }) => snapshots.length)
}; };
const handleActions = ({ target }) => const handleActions = ev => {
ev.stopPropagation();
ev.preventDefault();
onAction({ onAction({
name: target.value, name: ev.target.value,
items: selected items: selected
}); });
};
const _instances = forceArray(instances); const _instances = forceArray(instances);
const items = _instances.map((instance, i, all) => ( const items = _instances.map((instance, i, all) => {
<Item const { id } = instance;
key={instance.id}
{...instance} const isSelected = Boolean(find(selected, ['id', id]));
last={all.length - 1 === i} const isSubmitting = isSelected && submitting;
first={!i}
/> return (
)); <Item
key={id}
{...instance}
last={all.length - 1 === i}
first={!i}
loading={isSubmitting}
/>
);
});
const _loading = const _loading =
!items.length && loading ? ( !items.length && loading ? (
@ -61,19 +85,25 @@ export default ({
</ViewContainer> </ViewContainer>
) : null; ) : null;
const _error = error &&
!submitting && (
<Message error>
<MessageTitle>Ooops!</MessageTitle>
<MessageDescription>{error}</MessageDescription>
</Message>
);
return ( return (
<form <form>
onChange={() => handleSubmit(ctx => handleChange(ctx))}
onSubmit={handleSubmit}
>
<Row between="xs"> <Row between="xs">
<Col xs={10} sm={8} lg={6}> <Col xs={8} sm={8} lg={6}>
<Row> <Row>
<Col xs={7} sm={7} md={6} lg={6}> <Col xs={7} sm={7} md={6} lg={6}>
<FormGroup name="filter" reduxForm> <FormGroup name="filter" reduxForm>
<FormLabel>Filter instances</FormLabel> <FormLabel>Filter instances</FormLabel>
<Input <Input
placeholder="Search for name, state, tags, etc..." placeholder="Search for name, state, tags, etc..."
disabled={pristine && !items.length}
fluid fluid
/> />
</FormGroup> </FormGroup>
@ -98,9 +128,9 @@ export default ({
</Col> </Col>
</Row> </Row>
</Col> </Col>
<Col xs={2} sm={4} lg={6}> <Col xs={4} sm={4} lg={6}>
<Row end="xs"> <Row end="xs">
<Col xs={12} sm={3} md={3} lg={2}> <Col xs={6} sm={4} md={3} lg={2}>
<FormGroup> <FormGroup>
<FormLabel>&#8291;</FormLabel> <FormLabel>&#8291;</FormLabel>
<Select <Select
@ -148,10 +178,26 @@ export default ({
</Select> </Select>
</FormGroup> </FormGroup>
</Col> </Col>
<Col xs={6} sm={6} md={5} lg={2}>
<FormGroup>
<FormLabel>&#8291;</FormLabel>
<Button
type="button"
small
icon
fluid
onClick={() => onAction({ name: 'create' })}
>
<SmallOnly>+</SmallOnly>
<Medium>Create</Medium>
</Button>
</FormGroup>
</Col>
</Row> </Row>
</Col> </Col>
</Row> </Row>
{_loading} {_loading}
{_error}
{items} {items}
</form> </form>
); );

View File

@ -2,12 +2,19 @@ import React from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { compose, graphql } from 'react-apollo'; import { compose, graphql } from 'react-apollo';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { reduxForm, change } from 'redux-form';
import forceArray from 'force-array'; import forceArray from 'force-array';
import get from 'lodash.get'; import get from 'lodash.get';
import sortBy from 'lodash.sortby'; import sortBy from 'lodash.sortby';
import find from 'lodash.find'; import find from 'lodash.find';
import {
reduxForm,
SubmissionError,
stopSubmit,
startSubmit,
change
} from 'redux-form';
import { import {
ViewContainer, ViewContainer,
Title, Title,
@ -41,7 +48,7 @@ const List = ({
instances = [], instances = [],
loading = false, loading = false,
error, error,
onAction handleAction
}) => { }) => {
const _title = <Title>Instances</Title>; const _title = <Title>Instances</Title>;
const _instances = forceArray(instances); const _instances = forceArray(instances);
@ -63,8 +70,8 @@ const List = ({
{!_loading && _error} {!_loading && _error}
<InstanceListForm <InstanceListForm
instances={_instances} instances={_instances}
loading={loading} loading={_loading}
onAction={onAction} onAction={handleAction}
selected={selected} selected={selected}
/> />
</ViewContainer> </ViewContainer>
@ -118,6 +125,7 @@ export default compose(
: instances; : instances;
const selected = Object.keys(form) const selected = Object.keys(form)
.filter(key => Boolean(form[key]))
.map(name => find(values, ['name', name])) .map(name => find(values, ['name', name]))
.filter(Boolean) .filter(Boolean)
.map(({ id }) => find(instances, ['id', id])) .map(({ id }) => find(instances, ['id', id]))
@ -129,24 +137,41 @@ export default compose(
selected selected
}; };
}, },
(dispatch, { stop, start, reboot, history, location }) => ({ (
onAction: ({ name, items = [] }) => { dispatch,
{ stop, start, reboot, enableFw, disableFw, history, location }
) => ({
handleAction: ({ name, items = [] }) => {
const form = 'instance-list';
const types = { const types = {
stop: () => stop: () =>
Promise.all(items.map(({ id }) => stop({ variables: { id } }))), Promise.resolve(dispatch(startSubmit(form))).then(() =>
Promise.all(items.map(({ id }) => stop({ variables: { id } })))
),
start: () => start: () =>
Promise.all(items.map(({ id }) => start({ variables: { id } }))), Promise.resolve(dispatch(startSubmit(form))).then(() =>
Promise.all(items.map(({ id }) => start({ variables: { id } })))
),
reboot: () => reboot: () =>
Promise.all(items.map(({ id }) => reboot({ variables: { id } }))), Promise.resolve(dispatch(startSubmit(form))).then(() =>
Promise.all(items.map(({ id }) => reboot({ variables: { id } })))
),
resize: () => resize: () =>
Promise.resolve( Promise.resolve(
history.push(`/instances/~resize/${items.shift().name}`) history.push(`/instances/~resize/${items.shift().name}`)
), ),
enableFw: () => enableFw: () =>
Promise.all(items.map(({ id }) => enableFw({ variables: { id } }))), Promise.resolve(dispatch(startSubmit(form))).then(() =>
Promise.all(
items.map(({ id }) => enableFw({ variables: { id } }))
)
),
disableFw: () => disableFw: () =>
Promise.all( Promise.resolve(dispatch(startSubmit(form))).then(() =>
items.map(({ id }) => disableFw({ variables: { id } })) Promise.all(
items.map(({ id }) => disableFw({ variables: { id } }))
)
), ),
createSnap: () => createSnap: () =>
Promise.resolve( Promise.resolve(
@ -155,25 +180,32 @@ export default compose(
startSnap: () => startSnap: () =>
Promise.resolve( Promise.resolve(
history.push(`/instances/${items.shift().name}/snapshots`) history.push(`/instances/${items.shift().name}/snapshots`)
) ),
create: () =>
Promise.resolve(history.push(`/instances/~create-instance`))
}; };
const clearSelected = () => const handleError = error => {
throw new SubmissionError({
_error: error.graphQLErrors.map(({ message }) => message).join('\n')
});
};
const handleSuccess = error =>
dispatch( dispatch(
items.map(({ name: field }) => { items
const form = 'instance-list'; .map(({ name: field }) => change(form, field, false))
const value = false; .concat([stopSubmit(form)])
if (!field) {
return;
}
return change(form, field, value);
})
); );
const fn = types[name]; const fn = types[name];
return fn && fn().then(clearSelected);
return (
fn &&
fn()
.catch(handleError)
.then(handleSuccess)
);
} }
}) })
) )

View File

@ -16,14 +16,16 @@ import {
import GetInstance from '@graphql/get-instance.gql'; import GetInstance from '@graphql/get-instance.gql';
const Summary = ({ instance = {}, loading, error }) => { const Summary = ({ instance, loading, error }) => {
const { name } = instance; const { name } = instance || {};
const _title = <Title>Summary</Title>; const _title = <Title>Summary</Title>;
const _loading = !(loading && !name) ? null : <StatusLoader />; const _loading = loading && !name && <StatusLoader />;
const _summary = !_loading && <ReactJson src={instance} />; const _summary = !_loading && instance && <ReactJson src={instance} />;
const _error = !(error && !_loading) ? null : ( const _error = error &&
!_loading &&
!instance && (
<Message error> <Message error>
<MessageTitle>Ooops!</MessageTitle> <MessageTitle>Ooops!</MessageTitle>
<MessageDescription> <MessageDescription>