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,
Checkbox,
FormGroup,
QueryBreakpoints
QueryBreakpoints,
StatusLoader
} from 'joyent-ui-toolkit';
const { SmallOnly, Small } = QueryBreakpoints;
@ -24,7 +25,8 @@ const stateColor = {
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>
<CardView>
<CardMeta>
@ -34,23 +36,34 @@ export default ({ name, state, primary_ip, last, first }) => (
</FormGroup>
</CardAction>
<CardTitle to={`/instances/${name}`}>{name}</CardTitle>
<Small>
<CardLabel>{primary_ip}</CardLabel>
</Small>
<Small>
<CardLabel
color={stateColor[state]}
title={`The instance is ${state}`}
>
{titleCase(state)}
{loading && (
<CardLabel>
<StatusLoader small />
</CardLabel>
</Small>
<SmallOnly>
<CardLabel
color={stateColor[state]}
title={`The instance is ${state}`}
/>
</SmallOnly>
)}
{!loading && (
<Small>
<CardLabel>{primary_ip}</CardLabel>
</Small>
)}
{!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>
</CardView>
</Card>

View File

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

View File

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