feat(my-joy-beta): add loading and error to instance list actions
This commit is contained in:
parent
966d326f3f
commit
55811372fb
@ -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,9 +36,17 @@ export default ({ name, state, primary_ip, last, first }) => (
|
|||||||
</FormGroup>
|
</FormGroup>
|
||||||
</CardAction>
|
</CardAction>
|
||||||
<CardTitle to={`/instances/${name}`}>{name}</CardTitle>
|
<CardTitle to={`/instances/${name}`}>{name}</CardTitle>
|
||||||
|
{loading && (
|
||||||
|
<CardLabel>
|
||||||
|
<StatusLoader small />
|
||||||
|
</CardLabel>
|
||||||
|
)}
|
||||||
|
{!loading && (
|
||||||
<Small>
|
<Small>
|
||||||
<CardLabel>{primary_ip}</CardLabel>
|
<CardLabel>{primary_ip}</CardLabel>
|
||||||
</Small>
|
</Small>
|
||||||
|
)}
|
||||||
|
{!loading && (
|
||||||
<Small>
|
<Small>
|
||||||
<CardLabel
|
<CardLabel
|
||||||
color={stateColor[state]}
|
color={stateColor[state]}
|
||||||
@ -45,12 +55,15 @@ export default ({ name, state, primary_ip, last, first }) => (
|
|||||||
{titleCase(state)}
|
{titleCase(state)}
|
||||||
</CardLabel>
|
</CardLabel>
|
||||||
</Small>
|
</Small>
|
||||||
|
)}
|
||||||
|
{!loading && (
|
||||||
<SmallOnly>
|
<SmallOnly>
|
||||||
<CardLabel
|
<CardLabel
|
||||||
color={stateColor[state]}
|
color={stateColor[state]}
|
||||||
title={`The instance is ${state}`}
|
title={`The instance is ${state}`}
|
||||||
/>
|
/>
|
||||||
</SmallOnly>
|
</SmallOnly>
|
||||||
|
)}
|
||||||
</CardMeta>
|
</CardMeta>
|
||||||
</CardView>
|
</CardView>
|
||||||
</Card>
|
</Card>
|
||||||
|
@ -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) => {
|
||||||
|
const { id } = instance;
|
||||||
|
|
||||||
|
const isSelected = Boolean(find(selected, ['id', id]));
|
||||||
|
const isSubmitting = isSelected && submitting;
|
||||||
|
|
||||||
|
return (
|
||||||
<Item
|
<Item
|
||||||
key={instance.id}
|
key={id}
|
||||||
{...instance}
|
{...instance}
|
||||||
last={all.length - 1 === i}
|
last={all.length - 1 === i}
|
||||||
first={!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>⁣</FormLabel>
|
<FormLabel>⁣</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>⁣</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>
|
||||||
);
|
);
|
||||||
|
@ -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.resolve(dispatch(startSubmit(form))).then(() =>
|
||||||
Promise.all(
|
Promise.all(
|
||||||
items.map(({ id }) => disableFw({ variables: { id } }))
|
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)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
|
@ -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>
|
||||||
|
Loading…
Reference in New Issue
Block a user